import Client, {
  CommitmentLevel,
  SubscribeRequestAccountsDataSlice,
  SubscribeRequestFilterAccounts,
  SubscribeRequestFilterBlocks,
  SubscribeRequestFilterBlocksMeta,
  SubscribeRequestFilterEntry,
  SubscribeRequestFilterSlots,
  SubscribeRequestFilterTransactions,
} from "@triton-one/yellowstone-grpc";
import {GeyserClient, SubscribeRequestPing} from "@triton-one/yellowstone-grpc/dist/grpc/geyser";
import { PublicKey, VersionedTransactionResponse } from "@solana/web3.js";
import { Idl } from "@project-serum/anchor";
import { SolanaParser } from "@shyft-to/solana-transaction-parser";
import { TransactionFormatter } from "./utils/transaction-formatter";
import pumpFunIdl from "./idls/pump_0.1.0.json";
import { SolanaEventParser } from "./utils/event-parser";
import { bnLayoutFormatter } from "./utils/bn-layout-formatter";
import {RaydiumAmmParser} from "./parsers/raydium-amm-parser";
import {LogsParser} from "./parsers/logs-parser";
import {TimeUtil} from "./TimeUtil";
import {ChannelCredentials} from "@grpc/grpc-js";

interface SubscribeRequest {
  accounts: { [key: string]: SubscribeRequestFilterAccounts };
  slots: { [key: string]: SubscribeRequestFilterSlots };
  transactions: { [key: string]: SubscribeRequestFilterTransactions };
  transactionsStatus: { [key: string]: SubscribeRequestFilterTransactions };
  blocks: { [key: string]: SubscribeRequestFilterBlocks };
  blocksMeta: { [key: string]: SubscribeRequestFilterBlocksMeta };
  entry: { [key: string]: SubscribeRequestFilterEntry };
  commitment?: CommitmentLevel | undefined;
  accountsDataSlice: SubscribeRequestAccountsDataSlice[];
  ping?: SubscribeRequestPing | undefined;
}
const IX_PARSER = new SolanaParser([]);


const RAYDIUM_PUBLIC_KEY = RaydiumAmmParser.PROGRAM_ID;


const TXN_FORMATTER = new TransactionFormatter();

const LOGS_PARSER = new LogsParser();


const PUMP_FUN_PROGRAM_ID = new PublicKey(
  "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P",
);
const PUMP_FUN_IX_PARSER = new SolanaParser([]);
PUMP_FUN_IX_PARSER.addParserFromIdl(
  PUMP_FUN_PROGRAM_ID.toBase58(),
  pumpFunIdl as Idl,
);
const PUMP_FUN_EVENT_PARSER = new SolanaEventParser([], console);
PUMP_FUN_EVENT_PARSER.addParserFromIdl(
  PUMP_FUN_PROGRAM_ID.toBase58(),
  pumpFunIdl as Idl,
);

async function handleStream(client: Client, args: SubscribeRequest) {
  // Subscribe for events
  const stream = await client.subscribe();

  // Create `error` / `end` handler
  const streamClosed = new Promise<void>((resolve, reject) => {
    stream.on("error", (error) => {
      console.log("ERROR", error);
      reject(error);
      stream.end();
    });
    stream.on("end", () => {
      resolve();
    });
    stream.on("close", () => {
      resolve();
    });
  });

  // Handle updates
  stream.on("data", async (data) => {
    const startTime = Date.now()
    // console.log("arriveTime:", TimeUtil.getCurrentExactTime() + "  timeStamp:" + startTime)
    // console.log("data:\n", JSON.stringify(data));
    // todo 耗时?
    // const startTime1 = Date.now()
    const dataJSON = JSON.parse(JSON.stringify(data));
    // 耗时2ms
    // console.log("json spendTime:"+TimeUtil.timeDif(startTime1))
    if (dataJSON.filters.length != 0) {
      // const  startTime2 = Date.now()
      // 耗时1ms
      const extractedData = analyzeTransaction(dataJSON);
      // console.log("analyze spendTime:"+TimeUtil.timeDif(startTime2))
      // const startTime3 = Date.now()
      // console.log("\n" + JSON.stringify(extractedData))
      // console.log("print spendTime:"+TimeUtil.timeDif(startTime3))


      console.log("handle data spendTime:"+TimeUtil.timeDif(startTime))
    }


  });

  // Send subscribe request
  await new Promise<void>((resolve, reject) => {
    stream.write(args, (err: any) => {
      if (err === null || err === undefined) {
        resolve();
      } else {
        reject(err);
      }
    });
  }).catch((reason) => {
    console.error(reason);
    throw reason;
  });

  await streamClosed;
}

async function subscribeCommand(client: Client, args: SubscribeRequest) {
  while (true) {
    try {
      console.log("开始监听:"+args.accounts.toString())
      await handleStream(client, args);
    } catch (error) {
      console.error("Stream error, restarting in 1 second...", error);
      await new Promise((resolve) => setTimeout(resolve, 1000));
    }
  }
}

const client = new Client(
  "http://va.rpc.orbitflare.com:10000",
  undefined,
  undefined,
);

// const client = new GeyserClient('http://va.rpc.orbitflare.com:10000', ChannelCredentials.createInsecure());

const watchAddressArr = ["6SHBuawpromyjwqayF9rDFqGEa8GJRmntQQaNmk51tnp"]
const req: SubscribeRequest = {
  accounts: {},
  slots: {},
  transactions: {
    pumpFun: {
      vote: false,
      failed: false,
      signature: undefined,
      // accountInclude: [PUMP_FUN_PROGRAM_ID.toBase58()],

      accountInclude: watchAddressArr,
      accountExclude: [],
      accountRequired: [],


    },

    // raydiumLiquidityPoolV4: {
    //   vote: false,
    //   failed: false,
    //   signature: undefined,
    //   accountInclude: watchAddressArr,
    //   accountExclude: [],
    //   accountRequired: [],
    // },
  },
  transactionsStatus: {},
  entry: {},
  blocks: {},
  blocksMeta: {},
  accountsDataSlice: [],
  ping: undefined,
  commitment: CommitmentLevel.PROCESSED,
};

subscribeCommand(client, req);

function decodePumpFunTxn(tx: VersionedTransactionResponse) {
  if (tx.meta?.err) return;

  const paredIxs = PUMP_FUN_IX_PARSER.parseTransactionData(
    tx.transaction.message,
    tx.meta.loadedAddresses,
  );

  const pumpFunIxs = paredIxs.filter((ix) =>
    ix.programId.equals(PUMP_FUN_PROGRAM_ID),
  );

  if (pumpFunIxs.length === 0) return;
  const events = PUMP_FUN_EVENT_PARSER.parseEvent(tx);
  const result = { instructions: pumpFunIxs, events };
  bnLayoutFormatter(result);
  return result;
}

function decodeRaydiumTxn(tx: VersionedTransactionResponse) {
  if (tx.meta?.err) return;

  const parsedIxs = IX_PARSER.parseTransactionWithInnerInstructions(tx);

  const programIxs = parsedIxs.filter((ix) =>
      ix.programId.equals(RAYDIUM_PUBLIC_KEY),
  );

  if (programIxs.length === 0) return;
  const LogsEvent = LOGS_PARSER.parse(parsedIxs, tx.meta.logMessages);
  const result = { instructions: parsedIxs, events: LogsEvent };
  bnLayoutFormatter(result);
  return result;
}

function analyzeData(jsonData:any):any{


// 提取交易前后的代币余额信息
  const preTokenBalances = jsonData.transaction.transaction. meta.preTokenBalances;
  const postTokenBalances = jsonData.transaction.transaction. meta.postTokenBalances;

// 初始化结果数组
  const swapResults = preTokenBalances.map((preBalance, index) => {
    const postBalance = postTokenBalances[index];
    const mintAddress = preBalance.mint;
    const balanceChange = postBalance.uiTokenAmount.uiAmount - preBalance.uiTokenAmount.uiAmount;

    // 确定是买还是卖
    const isBuy = balanceChange > 0;

    // 返回结果对象
    return {
      mintAddress: mintAddress,
      isBuy: isBuy,
      tokenAmount: Math.abs(balanceChange), // 取绝对值，表示交换的代币数量
    };
  });

// 打印结果数组
  console.log(swapResults);
}

function analyzeTransaction(jsonData:any) {
  // 提取交易前后的代币余额信息
  const preTokenBalances = jsonData.transaction.transaction.meta.preTokenBalances;
  const postTokenBalances = jsonData.transaction.transaction.meta.postTokenBalances;


  let result = null

  // 遍历代币余额，找出发生变化的代币
  for (let i = 0; i < preTokenBalances.length; i++) {
    const preBalance = preTokenBalances[i];
    const postBalance = postTokenBalances[i];


    if (preBalance.mint !== "So11111111111111111111111111111111111111112") {
      const changeAmount = postBalance.uiTokenAmount.uiAmount - preBalance.uiTokenAmount.uiAmount;
      if (result == null) {
        result = {
          buyer: preBalance.owner,
          mint: preBalance.mint,
          isBuy: changeAmount > 0,
          changeAmount: changeAmount,
          uiAmount: preBalance.uiTokenAmount.uiAmount
        }
      } else {
        if (preBalance.uiTokenAmount.uiAmount < result.uiAmount) {
          result = {
            buyer: preBalance.owner,
            mint: preBalance.mint,
            isBuy: changeAmount > 0,
            changeAmount: changeAmount,
            uiAmount: preBalance.uiTokenAmount.uiAmount
          }
        }
      }
    }


  }
  return result
}
